/**
 *  Copyright 2010 Kyle O'Connor, Justin Arling, Mike Mercieca
 *
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  any later version.
 *  
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *  
 *  You should have received a copy of the GNU General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 * 
 */
package edu.drexel.sd0910.ece01.aqmon.manager.xbee;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.TreeMap;

import org.apache.log4j.Logger;
import org.json.simple.JSONObject;

import com.rapplogic.xbee.api.ApiId;
import com.rapplogic.xbee.api.AtCommand;
import com.rapplogic.xbee.api.AtCommandResponse;
import com.rapplogic.xbee.api.ErrorResponse;
import com.rapplogic.xbee.api.PacketListener;
import com.rapplogic.xbee.api.XBee;
import com.rapplogic.xbee.api.XBeeAddress64;
import com.rapplogic.xbee.api.XBeeException;
import com.rapplogic.xbee.api.XBeeResponse;
import com.rapplogic.xbee.api.AtCommandResponse.Status;
import com.rapplogic.xbee.api.wpan.NodeDiscover;
import com.rapplogic.xbee.api.zigbee.ZNetRxResponse;
import com.rapplogic.xbee.util.ByteUtils;

import edu.drexel.sd0910.ece01.aqmon.config.ConfigKeys;
import edu.drexel.sd0910.ece01.aqmon.data.ActiveNode;
import edu.drexel.sd0910.ece01.aqmon.data.DylosData;
import edu.drexel.sd0910.ece01.aqmon.data.GLatLng;
import edu.drexel.sd0910.ece01.aqmon.data.JSONFile;
import edu.drexel.sd0910.ece01.aqmon.data.calc.DataCalculatorThread;
import edu.drexel.sd0910.ece01.aqmon.gui.MainModel;
import edu.drexel.sd0910.ece01.aqmon.manager.Manager;
import edu.drexel.sd0910.ece01.aqmon.util.TimeUtils;
import gnu.io.CommPortIdentifier;

/**
 * Class to control XBee device (XBee API object)
 * 
 * @author Kyle O'Connor
 * 
 */
public class XBeeManager extends Manager implements PacketListener {

	private final static Logger log = Logger.getLogger(XBeeManager.class);
	
	private final static int DEFAULT_WAKE_TIME = 65;
	private final static int DEFAULT_NOSLEEP_TIME = 60; 

	private XBee xbee = null;
	private List<ActiveNode> activeNodes; //thread-safe
	private Map<String, Date> lastResponses;
	private Long ndTimeout = null;

	public XBeeManager(MainModel model, Properties props) {
		super(model, props);
		xbee = new XBee();
		xbee.addPacketListener(this);
		//create a thread-safe synchronized arraylist of active nodes
		activeNodes  = Collections.synchronizedList(new ArrayList<ActiveNode>());
		lastResponses = Collections.synchronizedSortedMap(new TreeMap<String, Date>());
		
		//create a single thread executor
		super.setExecutorType(NEW_SINGLE_THREAD_EXECUTOR);
	}

	@SuppressWarnings("unchecked")
	public Map<String, CommPortIdentifier> populatePorts() {
		Map<String, CommPortIdentifier> portMap = new TreeMap<String, CommPortIdentifier>();

		log.info("Initialize RXTX");
		Enumeration<CommPortIdentifier> ports = CommPortIdentifier.getPortIdentifiers();
		log.debug("Scanning for ports...");
		while (ports.hasMoreElements()) {
			CommPortIdentifier port = ports.nextElement();
			String type;
			switch (port.getPortType()) {
			case CommPortIdentifier.PORT_PARALLEL:
				type = "Parallel";
				break;
			case CommPortIdentifier.PORT_SERIAL:
				type = "Serial";
				break;
			default: // Shouldn't happen
				type = "Unknown";
				break;
			}

			String description = port.getName() + ": " + type + " ("
					+ ((port.isCurrentlyOwned()) ? "In Use" : "Available") + ")";
			log.debug(description);
			portMap.put(description, port);
		}

		return portMap;
	}

	public Map<XBeeAddress64, String> populateAddresses() {
		Map<XBeeAddress64, String> addressMap = new HashMap<XBeeAddress64, String>();

		Enumeration<?> keys = props.propertyNames();
		while (keys.hasMoreElements()) {
			String key = (String) keys.nextElement();
			if (key.startsWith("ADDR_")) {
				String address = props.getProperty(key);
				XBeeAddress64 xbeeAddress = new XBeeAddress64(address);
				String value = key.substring("ADDR_".length());
				addressMap.put(xbeeAddress, value);
			}
		}

		return addressMap;
	}

	public void connect(String port) throws NumberFormatException, XBeeException, InterruptedException {
		if (xbee.isConnected()) {
			log.info("XBee manager already connected. Disconnecting...");
			xbee.close();
		}
		xbee.open(port, Integer.parseInt(props.getProperty(ConfigKeys.BAUD_RATE_PROP)));
		log.debug("XBee connected on port " + port);
		model.setStatusText("Connected to XBee on " + port + "...");
		setSleepScheduleEnabled(false);
	}

	public void disconnect() {
		activeNodes.clear();
		xbee.close();
	}
	
	/* (non-Javadoc)
	 * @see edu.drexel.sd0910.ece01.aqmon.manager.Manager#shutdown()
	 */
	@Override
	public void shutdown() throws Exception {
		this.disconnect();
		stopExecutorService();
	}

	@Override
	public void startAllTasks() {
		setSleepScheduleEnabled(true);
	}

	public void processResponse(XBeeResponse response) {
		log.info("XBeeManager handling XBee response received");

		// handle AT command response
		if (response.getApiId() == ApiId.AT_RESPONSE) {
			AtCommandResponse ATresponse = (AtCommandResponse) response;
			// handle node discovery AT command response
			if (ATresponse.getCommand().equals("ND")) {
				NodeDiscover nd = NodeDiscover.parse(ATresponse);
				log.debug("Node discover response is: " + nd);
				// create ActiveNode object for each discovered device and maintain list
				String nodeIdentifier = model.getNodeIDFromAddress(nd.getNodeAddress64());
				ActiveNode node = new ActiveNode(nd.getNodeAddress64(), nodeIdentifier, nd.getRssi());
				if (!activeNodes.contains(node)) { 
					//prevent duplicates
					activeNodes.add(node);
				}
			} else {
				log.info("Ignoring unexpected AT response: " + ATresponse.getCommand());
			}
		} else if (response.getApiId() == ApiId.ZNET_RX_RESPONSE) {
			// handle data packet if capture enabled
			if (model.isCaptureEnabled()) {
				try {
					ZNetRxResponse packet = (ZNetRxResponse) response;
					DylosData data = new DylosData(packet, model.getNodeIDFromAddress(packet.getRemoteAddress64()));
					if (!data.isDeviceIdentification()) {

						// display on terminal window
						model.appendToTerminal("Got Packet from " + data.getNodeID() + ": (" + data.getDate()
								+ ") PM2.5: " + data.getParticleCounts()[0] + ", PM10: " + data.getParticleCounts()[1]);
						
						// save last response time of each node
						lastResponses.put(data.getNodeID(), data.getDate());
						
						// launch calculation thread using executor service
						getExecutorService().execute(new DataCalculatorThread(model, data));
					}
				} catch (Exception e) {
					log.error(e.toString());
					e.printStackTrace();
				}
			}
		} else if (response.getApiId() == ApiId.ERROR_RESPONSE) {
			ErrorResponse err = (ErrorResponse) response;
			if (err.getErrorMsg() != null) {
				log.error(err.toString());
			} else {
				log.error(err.getException().toString() + " [If being closed, then this should be OK]");
			}
		} else {
			log.info("Ignoring unexpected (unhandled) response: " + response);
		}
	}

	public void discoverNodes() throws XBeeException {
		//long timeout = getNodeDiscoveryTimeout();
		activeNodes.clear();
		
		log.debug("Sending node discover command");
		xbee.sendAsynchronous(new AtCommand("ND"));
	}

	public long getNodeDiscoveryTimeout() {
		if (ndTimeout == null) {
			log.debug("Determining Node Discovery timeout once");
			// get the Node discovery timeout, default is 6 seconds
			long defaultTimeout = Long.parseLong(props.getProperty(ConfigKeys.ND_TIMEOUT_PROP, "6000"));

			try {
				AtCommandResponse response = xbee.sendAtCommand(new AtCommand("NT"));
				//AtCommandResponse response = (AtCommandResponse) xbee.sendSynchronous(new AtCommand("NT"), 10000);

				if (response.isError()) {
					log.debug("Returning default timeout due to error response.");
					ndTimeout = new Long(defaultTimeout);
				}

				long nodeDiscoveryTimeout = ByteUtils.convertMultiByteToInt(response.getValue()) * 100;
				log.debug("Node discovery timeout is " + nodeDiscoveryTimeout + " milliseconds");
				ndTimeout = new Long(nodeDiscoveryTimeout);

			} catch (Exception e) {
				log.debug("Returning default timeout due to: " + e.toString());
				ndTimeout = new Long(defaultTimeout);
			}
		}
		return ndTimeout.longValue();
	}
	
	private void setSleepScheduleEnabled(boolean onSleepSchedule) {
		if (onSleepSchedule) {
			log.debug("Enabling sleep schedule");
			// enable the default sleep schedule
			int sleepTime = Integer.parseInt(props.getProperty(ConfigKeys.DEPLOYMENT_SLEEP_TIME_PROP, "535"));
			AtCommand sleepSched = new AtCommand("SP", TimeUtils.convertSecondsForSP(sleepTime));
			AtCommand wakeTime = new AtCommand("ST", TimeUtils.convertSecondsForST(DEFAULT_WAKE_TIME));
			
			// TODO figure out a way to manipulate these hex values and allow user to specify the property
			sleepSched = new AtCommand("SP", new int[] { 0xd0, 0xfc });
			wakeTime = new AtCommand("ST", new int[] { 0xfd, 0xe8 });

			forceSendAtCommand(sleepSched);
			forceSendAtCommand(wakeTime);

			log.debug("Enabled sleep schedule successfully");
		} else {
			log.debug("Disabling sleep schedule");
			// set to mostly awake mode
			AtCommand sleepSmall = new AtCommand("SP", TimeUtils.convertSecondsForSP(DEFAULT_NOSLEEP_TIME));
			AtCommand wakeTime = new AtCommand("ST", TimeUtils.convertSecondsForST(DEFAULT_WAKE_TIME));
			
			sleepSmall = new AtCommand("SP", new int[] { 0x17, 0x70 });
			wakeTime = new AtCommand("ST", new int[] { 0xfd, 0xe8 });

			forceSendAtCommand(sleepSmall);
			forceSendAtCommand(wakeTime);

			log.debug("Disabled sleep schedule successfully");
		}
	}
	
	private void forceSendAtCommand(AtCommand ATcommand) {
		AtCommandResponse response = null;
		do {
			try {
				response = xbee.sendAtCommand(ATcommand);
			} catch (XBeeException e) {
				// create an error response to continue while loop
				response = new AtCommandResponse();
				response.setStatus(Status.ERROR);
			}
		} while (!response.isOk());
	}

	@SuppressWarnings("unused")
	@Deprecated
	private void printIdentifyingInformation() throws XBeeException {
		log.info("DD is " + xbee.sendAtCommand(new AtCommand("DD")));
		log.info("SH is " + xbee.sendAtCommand(new AtCommand("SH")));
		log.info("SL is " + xbee.sendAtCommand(new AtCommand("SL")));
	}

	/**
	 * Returns the list of active nodes. Remember to synchronize on this list
	 * when iterating through it!
	 * 
	 * @return a <code>List</code> of <code>ActiveNode</code> objects
	 */
	public List<ActiveNode> getActiveNodesList() {
		return activeNodes;
	}
	
	/**
	 * Returns a map of the last node responses. Remember to synchronize on this
	 * map when iterating through it!
	 * 
	 * @return a <code>Map</code> of (<code>String</code>, <code>Date</code>)
	 *         pairs
	 */
	public Map<String, Date> getLastResponsesMap() {
		return lastResponses;
	}

	/**
	 * Export the node locations to a JSON file.
	 * 
	 * @return a <code>{@link JSONFile}</code> object
	 */
	@SuppressWarnings("unchecked")
	public JSONFile exportNodeLocations() {
		JSONObject markers = new JSONObject();
		synchronized (activeNodes) {
			List ptList = new LinkedList();

			Iterator<ActiveNode> i = activeNodes.iterator();
			while (i.hasNext()) {
				ActiveNode an = i.next();
				JSONObject pt = new JSONObject();

				pt.put("point", new GLatLng(an.getLatitude(), an.getLongitude()));

				@SuppressWarnings("unused")
				int indexAsInteger = Integer.parseInt(an.getNodeID().substring(an.getNodeID().indexOf('N') + 1));
				String indexAsString = an.getNodeID().substring(an.getNodeID().indexOf('N') + 1);
				pt.put("num", indexAsString);

				ptList.add(pt);
			}
			markers.put("markers", ptList);
		}
		return new JSONFile(markers, "points.json");
	}
	
}
